#region Header

// MS codename Velocity Distributed Cache Extensions.
// (c) 2009

#endregion


#region Usings

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Threading;

#endregion


namespace Caching.Extensions.Core.Impl
{
    /// <summary>
    /// Inprocess cache 
    /// </summary>
    internal sealed class InprocessCache : IDisposable, ISmartCacheLocalStorage
    {
        #region Private Data 

        private const string DebugCategory = "InprocessCache";
        private readonly IDictionary<SmartCacheKey, SmartCacheItem> m_cache;
        private readonly ReaderWriterLockSlim m_lock;
        private Timer m_timer;
        private readonly SmartCacheEventPublisher m_eventPublisher;

        #endregion Private Data


        public InprocessCache(TimeSpan cleanupInterval)
        {
            if (cleanupInterval.TotalSeconds < 0)
                throw new ArgumentException("Interval must be positive number", "cleanupInterval");

            m_lock = new ReaderWriterLockSlim(new LockRecursionPolicy());
            m_eventPublisher = new SmartCacheEventPublisher(this);
            m_cache = new Dictionary<SmartCacheKey, SmartCacheItem>();
            m_timer = new Timer(CleanupTask, this, cleanupInterval, cleanupInterval);
        }


        public bool Disposed { get; private set; }


        #region Public Methods 

        /// <summary>
        /// Get total count of items in cache.
        /// </summary>
        /// <returns>Total number of items (expired + non-expired)</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public int GetTotalCount()
        {
            try
            {
                m_lock.EnterReadLock();
                return m_cache.Count;
            }
            catch (Exception ex)
            {
                m_eventPublisher.LocalCacheError(ex, "GetTotalCount");
                throw;
            }
            finally
            {
                m_lock.ExitReadLock();
            }
        }


        /// <summary>
        /// Get count of non-expired items in cache
        /// </summary>
        /// <returns>Number of non-expired items</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
        public int GetLiveItemCount()
        {
            m_lock.EnterReadLock();
            try
            {
                return CountNonExpiredItems(DateTime.UtcNow);
            }
            catch (Exception ex)
            {
                m_eventPublisher.LocalCacheError(ex, "GetLIveItemCount");
                throw;
            }
            finally
            {
                m_lock.ExitReadLock();
            }
        }


        /// <summary>
        /// Put item into cache
        /// </summary>
        /// <param name="key">item key</param>
        /// <param name="item">item</param>
        /// <exception cref="ArgumentNullException">If key is null</exception>
        public void Put(SmartCacheKey key, SmartCacheItem item)
        {
            if (item == null)
            {
                Remove(key);
                return;
            }

            if (item.IsExpired(DateTime.UtcNow))
                return;

            m_lock.EnterWriteLock();
            try
            {
                m_cache[key] = item;
            }
            catch (Exception ex)
            {
                m_eventPublisher.LocalCacheError(ex, "Put");
                throw;
            }
            finally
            {
                m_lock.ExitWriteLock();
            }
            m_eventPublisher.LocalCacheItemOperation(LocalCacheItemOperation.Put, key);
        }


        /// <summary>
        /// Get item from cache
        /// </summary>
        /// <param name="key">item key</param>
        /// <returns>Cached data. Null if item not found or expired</returns>
        /// <exception cref="ArgumentNullException">If key is null</exception>
        public SmartCacheItem Get(SmartCacheKey key)
        {
            SmartCacheItem item;

            m_lock.EnterReadLock();
            try
            {
                if (m_cache.TryGetValue(key, out item))
                    if (item.IsExpired(DateTime.UtcNow))
                        item = null;
            }
            catch (Exception ex)
            {
                m_eventPublisher.LocalCacheError(ex, "Get");
                throw;
            }
            finally
            {
                m_lock.ExitReadLock();
            }
            m_eventPublisher.LocalCacheItemOperation(
                item != null ? LocalCacheItemOperation.Hit : LocalCacheItemOperation.Miss,
                key);
            return item;
        }


        /// <summary>
        /// Remove item from cache
        /// </summary>
        /// <param name="key">item key</param>
        /// <exception cref="ArgumentNullException">If key is null</exception>
        public void Remove(SmartCacheKey key)
        {
            m_lock.EnterWriteLock();
            try
            {
                m_cache.Remove(key);
            }
            catch (Exception ex)
            {
                m_eventPublisher.LocalCacheError(ex, "Remove");
                throw;
            }
            finally
            {
                m_lock.ExitWriteLock();
            }
            m_eventPublisher.LocalCacheItemOperation(LocalCacheItemOperation.Remove, key);
        }


        public SmartCacheEventPublisher NotificationEvents
        {
            get { return m_eventPublisher; }
        }

        #endregion Public Methods


        #region Private Methods 

        internal int CleanupExpired()
        {
            m_lock.EnterUpgradeableReadLock();

            try
            {
                Stopwatch watch = Stopwatch.StartNew();

                IList<SmartCacheKey> expiredItems = CollectExpiredItems(DateTime.UtcNow);
                int cleanedCount = RemoveItems(expiredItems);

                m_eventPublisher.LocalCacheCleanup(cleanedCount, watch.Elapsed);
                return cleanedCount;
            }
            catch (Exception ex)
            {
                m_eventPublisher.LocalCacheError(ex, "CleanupExpired");
                throw;
            }
            finally
            {
                m_lock.ExitUpgradeableReadLock();
            }
        }


        private int RemoveItems(ICollection<SmartCacheKey> items)
        {
            if (items.Count == 0)
                return 0;

            m_lock.EnterWriteLock();
            try
            {
                foreach (SmartCacheKey key in items)
                {
                    m_cache.Remove(key);
                }
                return items.Count;
            }
            finally
            {
                m_lock.ExitWriteLock();
            }
        }


        private IList<SmartCacheKey> CollectExpiredItems(DateTime time)
        {
            var expiredItems = new List<SmartCacheKey>(m_cache.Count);
            if (m_cache.Count == 0)
                return expiredItems;

            foreach (var pair in m_cache)
            {
                if (pair.Value.IsExpired(time))
                    expiredItems.Add(pair.Key);
            }
            return expiredItems;
        }


        private static void CleanupTask(object data)
        {
            var cache = (InprocessCache) data;
            if (cache == null)
                return;

            int expired = cache.CleanupExpired();
            Debug.WriteLine(
                string.Format(CultureInfo.InvariantCulture, "Background cleanup: [{0}] item(s) removed", expired),
                DebugCategory);
        }

        #endregion Private Methods


        #region Protected Methods 

        private int CountNonExpiredItems(DateTime dateTime)
        {
            int count = 0;
            foreach (SmartCacheItem cacheItem in m_cache.Values)
            {
                if (!cacheItem.IsExpired(dateTime))
                    count++;
            }
            return count;
        }


        #region Implementation of IDisposable

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        private void Dispose(bool disposing)
        {
            if (Disposed) return;

            if (m_timer != null)
            {
                m_timer.Dispose();
                m_timer = null;
            }

            if (disposing)
            {
                m_cache.Clear();
            }

            m_lock.Dispose();
            m_eventPublisher.Dispose();

            Disposed = true;
        }


        ~InprocessCache()
        {
            Dispose(false);
        }

        #endregion Implementation of IDisposable


        #endregion Protected Methods
    }
}